package com.vagavaga.tcpt;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;

import javax.swing.event.EventListenerList;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;

public class TransportServer implements ITransportServer {
    private static final Logger logger = Logger.getLogger(TransportServer.class);
    private EventListenerList listenerList = new EventListenerList();
    private Selector selector;
    private SelectorThread selectingThread;
    private final Object serverLock = new Object();

    public TransportServer() {
        super();
        turnOffLogging();
    }

    private static void turnOffLogging() {
        Logger root = Logger.getRootLogger();
        boolean rootIsConfigured = root.getAllAppenders().hasMoreElements();
        if (!rootIsConfigured) {
            Logger.getLogger(TransportServer.class.getPackage().getName()).setLevel(Level.OFF);
        }
    }

    private void start() throws IOException {
        if (selectingThread != null && selectingThread.isAlive()) return;
        ensureSelectorIsOpened();
        selectingThread = new SelectorThread(this, selector);
        selectingThread.setName("Selector Thread - " + selectingThread.getName());
        selectingThread.setDaemon(true);
        selectingThread.start();
    }

    public void stop() {
        synchronized (serverLock) {
            if (selectingThread != null) {
                logger.debug("Interrupting Selector Thread");
                selectingThread.interrupt();
            }
        }
    }

    public void ensureServerIsStarted() throws IOException {
        synchronized (serverLock) {
            start();
        }
    }

    public ServerSocket listen(int port) throws IOException {
        ensureServerIsStarted();

        logger.debug("Opening Server Socket");
        ServerSocketChannel channel = ServerSocketChannel.open();
        logger.debug("Configuring non blocking behaviour");
        channel.configureBlocking(false);
        // channel.socket().setReuseAddress(true);
        logger.debug("Binding server socket to an address");
        channel.socket().bind(new InetSocketAddress(port), 100);

        logger.debug("Registering server socket with selector for accept operation");
        channel.register(selector, SelectionKey.OP_ACCEPT, channel);
        return channel.socket();
    }

    private void ensureSelectorIsOpened() throws IOException {
        if (selector != null && selector.isOpen()) return;
        logger.debug("Opening selector");
        selector = Selector.open();
    }

    public IConnection connect(String host, int port) throws IOException {
        return connect(host, port, null);
    }
    
    public IConnection connect(String host, int port, Object attachment) throws IOException {
        ensureServerIsStarted();
        
        logger.debug("Starting connection process");
        Connection connection = createConnection(attachment);
        SocketChannel channel = SocketChannel.open();
        channel.configureBlocking(false);
        logger.debug("Registering connection [" + connection + "] with selector");
        connection.key = channel.register(selector, SelectionKey.OP_CONNECT, connection);
        channel.connect(new InetSocketAddress(host, port));
        return connection;
    }

    public void addServerListener(ITransportServerListener l) {

    }

    public void addConnectionListener(IConnectionListener l) {
        listenerList.add(IConnectionListener.class, l);
    }

    public void removeConnectionListener(IConnectionListener l) {
        listenerList.remove(IConnectionListener.class, l);
    }

    protected void fireConnected(Connection conn) {
        if (conn.connected) return;
        conn.connected = true;

        if (conn instanceof IConnectionListener) {
            ((IConnectionListener) conn).connected(conn);
        }

        logger.info("Connected [" + conn + "]");
        Object[] listeners = listenerList.getListenerList();

        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == IConnectionListener.class) {
                ((IConnectionListener) listeners[i + 1]).connected(conn);
            }
        }
    }

    protected void fireDisconnected(Connection conn) {
        logger.info("Disconnecting [" + conn + "]");
        if (!conn.connected) return;
        logger.info("Disconnected [" + conn + "]");
        conn.connected = false;

        if (conn instanceof IConnectionListener) {
            ((IConnectionListener) conn).disconnected(conn);
        }

        Object[] listeners = listenerList.getListenerList();

        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == IConnectionListener.class) {
                ((IConnectionListener) listeners[i + 1]).disconnected(conn);
            }
        }
    }

    protected void fireError(IConnection conn, IOException ex) {

        if (conn instanceof IConnectionListener) {
            ((IConnectionListener) conn).error(conn, ex);
        }

        Object[] listeners = listenerList.getListenerList();

        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == IConnectionListener.class) {
                ((IConnectionListener) listeners[i + 1]).error(conn, ex);
            }
        }
    }

    protected void fireDataReceived(IConnection conn, ByteBuffer buffer) {
        if (conn instanceof IConnectionListener) {
            ((IConnectionListener) conn).dataReceived(conn, buffer);
        }

        Object[] listeners = listenerList.getListenerList();

        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == IConnectionListener.class) {
                ((IConnectionListener) listeners[i + 1]).dataReceived(conn, buffer);
            }
        }
    }

    protected final Connection createConnection() {
        return createConnection(null);
    }

    protected Connection createConnection(Object attachment) {
        return new Connection(this, attachment);
    }

    protected static class Connection implements IConnection {
        public boolean connected;
        private static int connection_index;
        SelectionKey key;
        LinkedList<ByteBuffer> sendingQueue = new LinkedList<ByteBuffer>();
        private Object attachment;
        private int index;
        private final TransportServer server;
        protected Connection(TransportServer srv, Object attachment) {
            index = connection_index++;
            this.attachment = attachment;
            this.server = srv;
        }

        public void attach(Object att) {
            attachment = att;
        }

        public Object attachment() {
            return attachment;
        }

        public void detatch() {
            attach(null);
        }

        public void close() throws IOException {
            try {
            logger.debug("Closing connection " + this);
            key.channel().close();
            sendingQueue.clear();
            } finally {
                server.fireDisconnected(this);
            }
        }

        public void send(ByteBuffer buffer) throws IOException {
            synchronized (sendingQueue) {
                while (sendingQueue.size() > 3) {
                    try {
                        sendingQueue.wait(100);
                    } catch (InterruptedException e) {
                    }
                }
                if (key != null && key.isValid()) {
                    logger.debug("Expressing interest to write on " + this);
                    sendingQueue.addLast(buffer);
                    key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
                } else {
                    throw new IOException("Invalid connection");
                }
            }
        }

        @Override
        public String toString() {
            return "Connection : " + index;
        }
    }
}
